<?php

namespace App\Library\Traits;

use DateTimeInterface;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\DB;

trait BaseModelTrait
{
    use HasFactory, BaseTrait;

    /**
     * insert
     * @param array $values
     * @return bool
     */
    public static function insert(array $values): bool
    {
        return self::query()->insert(humpToLine($values));
    }

    /**
     * insertForModel
     * @param array|string $obj
     * @param mixed $val
     * @return Model
     */
    public static function insertForModel(array|string $obj, mixed $val = null): Model
    {
        $class = get_called_class();
        $model = new $class;
        $columns = DB::select('show columns from `' . $model->getTable() . '`');
        $fillAble = array_column($columns, 'Field');
        if (is_string($obj)) {
            $key = humpToLine($obj);
            if ($model->getKeyName() === $key) return $model;
            else if (!in_array($key, $fillAble)) return $model;
            $model->$key = $val;
        } else {
            foreach ($obj as $k => $v) {
                $key = humpToLine($k);
                if ($model->getKeyName() === $key) continue;
                else if ($key === Model::CREATED_AT) {
                    if (!in_array($model->getTable(), self::createdAtExcept())) continue;
                } else if ($key === Model::UPDATED_AT) {
                    if (!in_array($model->getTable(), self::createdAtExcept())) continue;
                } else if (!in_array($key, $fillAble)) continue;
                $model->$key = $v;
            }
        }
        $field = 'serial_number';
        if (in_array($field, $fillAble) && empty($model->serial_number)) {
            foreach ($columns as $value) {
                if ($value->Field !== $field) continue;
                $length = strtr($value->Type, ['varchar' => '', 'char' => '', '(' => '', ')' => '']) * 1;
            }
            $model->serial_number = self::getSerialNumber($length ?? 20, $fillAble);
        }
        $model->save();
        return $model;
    }

    /**
     * updateForModel
     * @param mixed $arr
     * @param Model $model
     * @return Model
     */
    public static function updateForModel(array $arr, Model $model): Model
    {
        foreach ($arr as $k => $v) {
            $key = humpToLine($k);
            if (!in_array($key, $model->fillable)) continue;
            else if ($key === 'serial_number') continue;
            $model->$key = $v;
        }
        $model->save();
        return $model;
    }

    /**
     * detail
     * @param int $id
     * @param array|string $columns
     * @return ?Model
     */
    public static function detail(int $id, array|string $columns = ['*']): ?Model
    {
        return self::query()->find($id, humpToLine($columns));
    }

    /**
     * findByWhere
     * @param array|string|int $where
     * @param mixed|null $val
     * @param array|string $columns
     * @return ?Model
     */
    public static function findByWhere(array|string|int $where, mixed $val = null, array|string $columns = ['*']): ?Model
    {
        return self::queryByWhere($where, $val)->first(humpToLine($columns));
    }

    /**
     * valueByWhere
     * @param array|string|int $where
     * @param mixed|null $val
     * @param string $column
     * @return mixed
     */
    public static function valueByWhere(array|string|int $where, mixed $val, string $column): mixed
    {
        return self::queryByWhere($where, $val)->value(humpToLine($column));
    }

    /**
     * valueByWhere
     * @param array|string|int $where
     * @param mixed|null $val
     * @param string $column
     * @return mixed
     */
    public static function sumByWhere(array|string|int $where, mixed $val, string $column): mixed
    {
        return self::queryByWhere($where, $val)->sum(humpToLine($column));
    }

    /**
     * countByWhere
     * @param array|string|int $where
     * @param mixed|null $val
     * @return int
     */
    public static function countByWhere(array|string|int $where, mixed $val = null): int
    {
        return self::queryByWhere($where, $val)->count();
    }

    /**
     * existsByWhere
     * @param array|string|int $where
     * @param mixed|null $val
     * @return bool
     */
    public static function existsByWhere(array|string|int $where, mixed $val = null): bool
    {
        return self::queryByWhere($where, $val)->exists();
    }

    /**
     * updateByWhere
     * @param array $data
     * @param array|string|int $where
     * @param mixed|null $val
     * @return int
     */
    public static function updateByWhere(array $data, array|string|int $where, mixed $val = null): int
    {
        return self::queryByWhere($where, $val)->update(humpToLine($data));
    }

    /**
     * deleteByWhere
     * @param array|string|int $where
     * @param mixed|null $val
     * @return int
     */
    public static function deleteByWhere(array|string|int $where, mixed $val = null): int
    {
        return self::queryByWhere($where, $val)->delete();
    }

    /**
     * getByWhere
     * @param array|string|int $where
     * @param mixed|null $val
     * @param array|string $columns
     * @return Collection
     */
    public static function getByWhere(array|string|int $where, mixed $val = null, array|string $columns = ['*']): Collection
    {
        return self::queryByWhere($where, $val)->get(humpToLine($columns));
    }

    /**
     * deleteById
     * @param int $id
     * @return int
     */
    public static function deleteById(int $id): int
    {
        return self::query()->id($id)->delete();
    }

    /**
     * updateById
     * @param int|array $id
     * @param array|int $arr
     * @return int
     */
    public static function updateById(int|array $id, array|int $arr): int
    {
        if (is_int($id)) return self::query()->id($id)->update(humpToLine($arr));
        else return self::query()->id($arr)->update(humpToLine($id));
    }

    /**
     * incById
     * @param int $id
     * @param string $column
     * @param float|int $amount
     * @return int
     */
    public static function incById(int $id, string $column, float|int $amount = 1): int
    {
        return self::query()->id($id)->increment(humpToLine($column), $amount);
    }

    /**
     * decById
     * @param int $id
     * @param string $column
     * @param float|int $amount
     * @return int
     */
    public static function decById(int $id, string $column, float|int $amount = 1): int
    {
        return self::query()->id($id)->decrement(humpToLine($column), $amount);
    }

    /**
     * decById
     * @param array|string|int $where
     * @param mixed|null $val
     * @return Builder
     */
    public static function queryByWhere(array|string|int $where, mixed $val = null): Builder
    {
        $builder = self::query();
        if (is_int($where)) $builder->id($where);
        else if (is_string($where)) $builder->where(humpToLine($where), $val);
        else {
            foreach ($where as $key => $value) {
                if (in_array($key, ['orderBy', 'orderByDesc'])) $builder->$key(is_null($value) ? 'id' : humpToLine($value));
                else if ($key === 'whereIn') {
                    if (!empty($value[0])) $builder->whereIn(humpToLine($value[0]), $value[1]);
                    else foreach ($value as $k => $v) $builder->whereIn(humpToLine($k), $v);
                } else if ($key === 'whereRaw') $builder->whereRaw($value);
                else {
                    if (is_array($value)) $builder->where(humpToLine($key), $value[0], $value[1]);
                    else $builder->where(humpToLine($key), $value);
                }
            }
        }
        return $builder;
    }


    /**
     * Desc scopeId
     * @param $query
     * @param int|null $id
     * @return Builder
     */
    public function scopeId($query, int|null $id = 0): Builder
    {
        return $query->where($this->getTable() . '.id', $id);
    }

    /**
     * Desc serializeDate
     * @param DateTimeInterface $date
     * @return string
     */
    public function serializeDate(DateTimeInterface $date): string
    {
        return $date->format('Y-m-d H:i:s');
    }

    private static function createdAtExcept(): array
    {
        return [];
    }
}
